agentmux_srv\backend\wconfig/
types.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Configuration type definitions: settings, themes, widgets, connections, bookmarks.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::backend::obj::MetaMapType;
12
13// ---- Serde helpers (used by skip_serializing_if attributes) ----
14
15pub(crate) fn is_false(v: &bool) -> bool {
16    !v
17}
18
19pub(crate) fn is_zero_f64(v: &f64) -> bool {
20    *v == 0.0
21}
22
23pub(crate) fn is_zero_f32(v: &f32) -> bool {
24    *v == 0.0
25}
26
27pub(crate) fn is_zero_i32(v: &i32) -> bool {
28    *v == 0
29}
30
31// ---- SettingsType ----
32
33/// Application settings. Matches Go's `wconfig.SettingsType` JSON tags.
34/// Fields use pointer-like `Option` for nullable booleans/numbers
35/// to distinguish "not set" from "false/0".
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct SettingsType {
38    // -- App settings --
39    #[serde(rename = "app:*", default, skip_serializing_if = "is_false")]
40    pub app_clear: bool,
41
42    #[serde(rename = "app:globalhotkey", default, skip_serializing_if = "String::is_empty")]
43    pub app_global_hotkey: String,
44
45    #[serde(rename = "app:dismissarchitecturewarning", default, skip_serializing_if = "is_false")]
46    pub app_dismiss_architecture_warning: bool,
47
48    #[serde(rename = "app:defaultnewblock", default, skip_serializing_if = "String::is_empty")]
49    pub app_default_new_block: String,
50
51    #[serde(rename = "app:showoverlayblocknums", default, skip_serializing_if = "Option::is_none")]
52    pub app_show_overlay_block_nums: Option<bool>,
53
54    // -- Terminal settings --
55    #[serde(rename = "term:*", default, skip_serializing_if = "is_false")]
56    pub term_clear: bool,
57
58    #[serde(rename = "term:fontsize", default, skip_serializing_if = "is_zero_f64")]
59    pub term_font_size: f64,
60
61    #[serde(rename = "term:fontfamily", default, skip_serializing_if = "String::is_empty")]
62    pub term_font_family: String,
63
64    #[serde(rename = "term:theme", default, skip_serializing_if = "String::is_empty")]
65    pub term_theme: String,
66
67    #[serde(rename = "term:disablewebgl", default, skip_serializing_if = "is_false")]
68    pub term_disable_web_gl: bool,
69
70    #[serde(rename = "term:localshellpath", default, skip_serializing_if = "String::is_empty")]
71    pub term_local_shell_path: String,
72
73    #[serde(rename = "term:localshellopts", default, skip_serializing_if = "Vec::is_empty")]
74    pub term_local_shell_opts: Vec<String>,
75
76    #[serde(rename = "term:scrollback", default, skip_serializing_if = "Option::is_none")]
77    pub term_scrollback: Option<i64>,
78
79    #[serde(rename = "term:copyonselect", default, skip_serializing_if = "Option::is_none")]
80    pub term_copy_on_select: Option<bool>,
81
82    #[serde(rename = "term:transparency", default, skip_serializing_if = "Option::is_none")]
83    pub term_transparency: Option<f64>,
84
85    #[serde(rename = "term:allowbracketedpaste", default, skip_serializing_if = "Option::is_none")]
86    pub term_allow_bracketed_paste: Option<bool>,
87
88    #[serde(rename = "term:shiftenternewline", default, skip_serializing_if = "Option::is_none")]
89    pub term_shift_enter_newline: Option<bool>,
90
91    /// Maximum runtime in hours before the watchdog kills an agent pane.
92    /// 0 (default) disables the limit.
93    #[serde(rename = "term:agentmaxruntimehours", default, skip_serializing_if = "is_zero_f64")]
94    pub term_agent_max_runtime_hours: f64,
95
96    /// Minutes of PTY silence before the watchdog kills an idle agent pane.
97    /// 0 (default) disables the limit.
98    #[serde(rename = "term:agentidletimeoutmins", default, skip_serializing_if = "is_zero_f64")]
99    pub term_agent_idle_timeout_mins: f64,
100
101    // -- Command settings --
102    #[serde(rename = "cmd:env", default, skip_serializing_if = "HashMap::is_empty")]
103    pub cmd_env: HashMap<String, String>,
104
105    // -- Block header settings --
106    #[serde(rename = "blockheader:*", default, skip_serializing_if = "is_false")]
107    pub block_header_clear: bool,
108
109    #[serde(rename = "blockheader:showblockids", default, skip_serializing_if = "is_false")]
110    pub block_header_show_block_ids: bool,
111
112    // -- Preview settings --
113    #[serde(rename = "preview:showhiddenfiles", default, skip_serializing_if = "Option::is_none")]
114    pub preview_show_hidden_files: Option<bool>,
115
116    // -- Tab settings --
117    #[serde(rename = "tab:preset", default, skip_serializing_if = "String::is_empty")]
118    pub tab_preset: String,
119
120    // -- Widget settings --
121    #[serde(rename = "widget:*", default, skip_serializing_if = "is_false")]
122    pub widget_clear: bool,
123
124    #[serde(rename = "widget:showhelp", default, skip_serializing_if = "Option::is_none")]
125    pub widget_show_help: Option<bool>,
126
127    #[serde(rename = "widget:icononly", default, skip_serializing_if = "Option::is_none")]
128    pub widget_icon_only: Option<bool>,
129
130    // -- Window settings --
131    #[serde(rename = "window:*", default, skip_serializing_if = "is_false")]
132    pub window_clear: bool,
133
134    #[serde(rename = "window:transparent", default, skip_serializing_if = "is_false")]
135    pub window_transparent: bool,
136
137    #[serde(rename = "window:blur", default, skip_serializing_if = "is_false")]
138    pub window_blur: bool,
139
140    #[serde(rename = "window:opacity", default, skip_serializing_if = "Option::is_none")]
141    pub window_opacity: Option<f64>,
142
143    #[serde(rename = "window:bgcolor", default, skip_serializing_if = "String::is_empty")]
144    pub window_bg_color: String,
145
146    #[serde(rename = "window:reducedmotion", default, skip_serializing_if = "is_false")]
147    pub window_reduced_motion: bool,
148
149    #[serde(rename = "window:tilegapsize", default, skip_serializing_if = "Option::is_none")]
150    pub window_tile_gap_size: Option<i64>,
151
152    #[serde(rename = "window:showmenubar", default, skip_serializing_if = "is_false")]
153    pub window_show_menu_bar: bool,
154
155    #[serde(rename = "window:nativetitlebar", default, skip_serializing_if = "is_false")]
156    pub window_native_title_bar: bool,
157
158    #[serde(rename = "window:disablehardwareacceleration", default, skip_serializing_if = "is_false")]
159    pub window_disable_hardware_acceleration: bool,
160
161    #[serde(rename = "window:maxtabcachesize", default, skip_serializing_if = "is_zero_i32")]
162    pub window_max_tab_cache_size: i32,
163
164    #[serde(rename = "window:magnifiedblockopacity", default, skip_serializing_if = "Option::is_none")]
165    pub window_magnified_block_opacity: Option<f64>,
166
167    #[serde(rename = "window:magnifiedblocksize", default, skip_serializing_if = "Option::is_none")]
168    pub window_magnified_block_size: Option<f64>,
169
170    #[serde(rename = "window:magnifiedblockblurprimarypx", default, skip_serializing_if = "Option::is_none")]
171    pub window_magnified_block_blur_primary_px: Option<i64>,
172
173    #[serde(rename = "window:magnifiedblockblursecondarypx", default, skip_serializing_if = "Option::is_none")]
174    pub window_magnified_block_blur_secondary_px: Option<i64>,
175
176    #[serde(rename = "window:confirmclose", default, skip_serializing_if = "is_false")]
177    pub window_confirm_close: bool,
178
179    #[serde(rename = "window:savelastwindow", default, skip_serializing_if = "is_false")]
180    pub window_save_last_window: bool,
181
182    #[serde(rename = "window:dimensions", default, skip_serializing_if = "String::is_empty")]
183    pub window_dimensions: String,
184
185    #[serde(rename = "window:zoom", default, skip_serializing_if = "Option::is_none")]
186    pub window_zoom: Option<f64>,
187
188    // -- Telemetry settings --
189    #[serde(rename = "telemetry:*", default, skip_serializing_if = "is_false")]
190    pub telemetry_clear: bool,
191
192    #[serde(rename = "telemetry:enabled", default, skip_serializing_if = "is_false")]
193    pub telemetry_enabled: bool,
194
195    #[serde(rename = "telemetry:interval", default, skip_serializing_if = "is_zero_f64")]
196    pub telemetry_interval: f64,
197
198    #[serde(rename = "telemetry:numpoints", default, skip_serializing_if = "Option::is_none")]
199    pub telemetry_numpoints: Option<i64>,
200
201    // -- Connection settings --
202    #[serde(rename = "conn:*", default, skip_serializing_if = "is_false")]
203    pub conn_clear: bool,
204
205    // -- Network settings --
206    #[serde(rename = "network:lan_discovery", default, skip_serializing_if = "is_false")]
207    pub network_lan_discovery: bool,
208
209    // -- Voice settings --
210    //
211    // `voice:enabled` globally controls whether the per-pane microphone
212    // button is rendered. The default at the UX layer is "enabled" — the
213    // frontend treats `undefined`/absent as enabled, so we only need to
214    // model the explicit-disable case here. Users set this to `false` in
215    // `settings.json` to fully hide the buttons across all panes.
216    //
217    // Spec: docs/specs/SPEC_VOICE_INPUT_PER_PANE_2026_05_19.md §7 Phase 3.
218    #[serde(rename = "voice:enabled", default, skip_serializing_if = "Option::is_none")]
219    pub voice_enabled: Option<bool>,
220
221    /// Catch-all for unknown/dynamic keys (e.g. `widget:hidden@defwidget@sysinfo`).
222    /// These pass through serde unchanged so the frontend can access them as flat settings keys.
223    #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
224    pub extra: HashMap<String, serde_json::Value>,
225}
226
227// ---- Supporting config types ----
228
229/// MIME type display configuration.
230#[derive(Debug, Clone, Default, Serialize, Deserialize)]
231pub struct MimeTypeConfigType {
232    #[serde(default, skip_serializing_if = "String::is_empty")]
233    pub icon: String,
234
235    #[serde(default, skip_serializing_if = "String::is_empty")]
236    pub color: String,
237}
238
239/// File definition for block widgets.
240#[derive(Debug, Clone, Default, Serialize, Deserialize)]
241pub struct FileDef {
242    #[serde(default, skip_serializing_if = "String::is_empty")]
243    pub content: String,
244
245    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
246    pub meta: HashMap<String, Value>,
247}
248
249/// Block definition for widgets.
250#[derive(Debug, Clone, Default, Serialize, Deserialize)]
251pub struct BlockDef {
252    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
253    pub files: HashMap<String, FileDef>,
254
255    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
256    pub meta: MetaMapType,
257}
258
259/// Widget configuration.
260#[derive(Debug, Clone, Default, Serialize, Deserialize)]
261pub struct WidgetConfigType {
262    #[serde(rename = "display:order", default, skip_serializing_if = "is_zero_f64")]
263    pub display_order: f64,
264
265    #[serde(rename = "display:hidden", default, skip_serializing_if = "is_false")]
266    pub display_hidden: bool,
267
268    /// Whether this widget is pinned to the action bar by default on new installs.
269    /// Once the user has a `widget:pinned` setting this field is ignored.
270    #[serde(rename = "display:pinned", default, skip_serializing_if = "is_false")]
271    pub display_pinned: bool,
272
273    #[serde(default, skip_serializing_if = "String::is_empty")]
274    pub icon: String,
275
276    #[serde(default, skip_serializing_if = "String::is_empty")]
277    pub color: String,
278
279    #[serde(default, skip_serializing_if = "String::is_empty")]
280    pub label: String,
281
282    #[serde(default, skip_serializing_if = "String::is_empty")]
283    pub description: String,
284
285    #[serde(default, skip_serializing_if = "is_false")]
286    pub magnified: bool,
287
288    #[serde(rename = "blockdef", default)]
289    pub block_def: BlockDef,
290}
291
292/// Terminal color theme.
293#[derive(Debug, Clone, Default, Serialize, Deserialize)]
294pub struct TermThemeType {
295    #[serde(rename = "display:name", default, skip_serializing_if = "String::is_empty")]
296    pub display_name: String,
297
298    #[serde(rename = "display:order", default, skip_serializing_if = "is_zero_f64")]
299    pub display_order: f64,
300
301    #[serde(default, skip_serializing_if = "String::is_empty")]
302    pub black: String,
303    #[serde(default, skip_serializing_if = "String::is_empty")]
304    pub red: String,
305    #[serde(default, skip_serializing_if = "String::is_empty")]
306    pub green: String,
307    #[serde(default, skip_serializing_if = "String::is_empty")]
308    pub yellow: String,
309    #[serde(default, skip_serializing_if = "String::is_empty")]
310    pub blue: String,
311    #[serde(default, skip_serializing_if = "String::is_empty")]
312    pub magenta: String,
313    #[serde(default, skip_serializing_if = "String::is_empty")]
314    pub cyan: String,
315    #[serde(default, skip_serializing_if = "String::is_empty")]
316    pub white: String,
317
318    #[serde(rename = "brightBlack", default, skip_serializing_if = "String::is_empty")]
319    pub bright_black: String,
320    #[serde(rename = "brightRed", default, skip_serializing_if = "String::is_empty")]
321    pub bright_red: String,
322    #[serde(rename = "brightGreen", default, skip_serializing_if = "String::is_empty")]
323    pub bright_green: String,
324    #[serde(rename = "brightYellow", default, skip_serializing_if = "String::is_empty")]
325    pub bright_yellow: String,
326    #[serde(rename = "brightBlue", default, skip_serializing_if = "String::is_empty")]
327    pub bright_blue: String,
328    #[serde(rename = "brightMagenta", default, skip_serializing_if = "String::is_empty")]
329    pub bright_magenta: String,
330    #[serde(rename = "brightCyan", default, skip_serializing_if = "String::is_empty")]
331    pub bright_cyan: String,
332    #[serde(rename = "brightWhite", default, skip_serializing_if = "String::is_empty")]
333    pub bright_white: String,
334
335    #[serde(default, skip_serializing_if = "String::is_empty")]
336    pub gray: String,
337    #[serde(rename = "cmdtext", default, skip_serializing_if = "String::is_empty")]
338    pub cmd_text: String,
339    #[serde(default, skip_serializing_if = "String::is_empty")]
340    pub foreground: String,
341    #[serde(rename = "selectionBackground", default, skip_serializing_if = "String::is_empty")]
342    pub selection_background: String,
343    #[serde(default, skip_serializing_if = "String::is_empty")]
344    pub background: String,
345    #[serde(default, skip_serializing_if = "String::is_empty")]
346    pub cursor: String,
347}
348
349/// Web bookmark.
350#[derive(Debug, Clone, Default, Serialize, Deserialize)]
351pub struct WebBookmark {
352    #[serde(default)]
353    pub url: String,
354
355    #[serde(default, skip_serializing_if = "String::is_empty")]
356    pub title: String,
357
358    #[serde(default, skip_serializing_if = "String::is_empty")]
359    pub icon: String,
360
361    #[serde(rename = "iconcolor", default, skip_serializing_if = "String::is_empty")]
362    pub icon_color: String,
363
364    #[serde(rename = "iconurl", default, skip_serializing_if = "String::is_empty")]
365    pub icon_url: String,
366
367    #[serde(rename = "display:order", default, skip_serializing_if = "is_zero_f64")]
368    pub display_order: f64,
369}
370
371/// Per-connection configuration keywords.
372/// Matches Go's `wconfig.ConnKeywords`.
373#[derive(Debug, Clone, Default, Serialize, Deserialize)]
374pub struct ConnKeywords {
375    // -- Connection settings --
376    #[serde(rename = "conn:shellpath", default, skip_serializing_if = "String::is_empty")]
377    pub conn_shell_path: String,
378
379    #[serde(rename = "conn:ignoresshconfig", default, skip_serializing_if = "Option::is_none")]
380    pub conn_ignore_ssh_config: Option<bool>,
381
382    // -- Display settings --
383    #[serde(rename = "display:hidden", default, skip_serializing_if = "Option::is_none")]
384    pub display_hidden: Option<bool>,
385
386    #[serde(rename = "display:order", default, skip_serializing_if = "is_zero_f32")]
387    pub display_order: f32,
388
389    // -- Terminal settings --
390    #[serde(rename = "term:*", default, skip_serializing_if = "is_false")]
391    pub term_clear: bool,
392
393    #[serde(rename = "term:fontsize", default, skip_serializing_if = "is_zero_f64")]
394    pub term_font_size: f64,
395
396    #[serde(rename = "term:fontfamily", default, skip_serializing_if = "String::is_empty")]
397    pub term_font_family: String,
398
399    #[serde(rename = "term:theme", default, skip_serializing_if = "String::is_empty")]
400    pub term_theme: String,
401
402    // -- Command settings --
403    #[serde(rename = "cmd:env", default, skip_serializing_if = "HashMap::is_empty")]
404    pub cmd_env: HashMap<String, String>,
405
406    #[serde(rename = "cmd:initscript", default, skip_serializing_if = "String::is_empty")]
407    pub cmd_init_script: String,
408
409    #[serde(rename = "cmd:initscript.sh", default, skip_serializing_if = "String::is_empty")]
410    pub cmd_init_script_sh: String,
411
412    #[serde(rename = "cmd:initscript.bash", default, skip_serializing_if = "String::is_empty")]
413    pub cmd_init_script_bash: String,
414
415    #[serde(rename = "cmd:initscript.zsh", default, skip_serializing_if = "String::is_empty")]
416    pub cmd_init_script_zsh: String,
417
418    #[serde(rename = "cmd:initscript.pwsh", default, skip_serializing_if = "String::is_empty")]
419    pub cmd_init_script_pwsh: String,
420
421    #[serde(rename = "cmd:initscript.fish", default, skip_serializing_if = "String::is_empty")]
422    pub cmd_init_script_fish: String,
423
424    // -- SSH settings --
425    #[serde(rename = "ssh:user", default, skip_serializing_if = "Option::is_none")]
426    pub ssh_user: Option<String>,
427
428    #[serde(rename = "ssh:hostname", default, skip_serializing_if = "Option::is_none")]
429    pub ssh_hostname: Option<String>,
430
431    #[serde(rename = "ssh:port", default, skip_serializing_if = "Option::is_none")]
432    pub ssh_port: Option<String>,
433
434    #[serde(rename = "ssh:identityfile", default, skip_serializing_if = "Vec::is_empty")]
435    pub ssh_identity_file: Vec<String>,
436
437    #[serde(rename = "ssh:batchmode", default, skip_serializing_if = "Option::is_none")]
438    pub ssh_batch_mode: Option<bool>,
439
440    #[serde(rename = "ssh:pubkeyauthentication", default, skip_serializing_if = "Option::is_none")]
441    pub ssh_pubkey_authentication: Option<bool>,
442
443    #[serde(rename = "ssh:passwordauthentication", default, skip_serializing_if = "Option::is_none")]
444    pub ssh_password_authentication: Option<bool>,
445
446    #[serde(rename = "ssh:kbdinteractiveauthentication", default, skip_serializing_if = "Option::is_none")]
447    pub ssh_kbd_interactive_authentication: Option<bool>,
448
449    #[serde(rename = "ssh:preferredauthentications", default, skip_serializing_if = "Vec::is_empty")]
450    pub ssh_preferred_authentications: Vec<String>,
451
452    #[serde(rename = "ssh:addkeystoagent", default, skip_serializing_if = "Option::is_none")]
453    pub ssh_add_keys_to_agent: Option<bool>,
454
455    #[serde(rename = "ssh:identityagent", default, skip_serializing_if = "Option::is_none")]
456    pub ssh_identity_agent: Option<String>,
457
458    #[serde(rename = "ssh:identitiesonly", default, skip_serializing_if = "Option::is_none")]
459    pub ssh_identities_only: Option<bool>,
460
461    #[serde(rename = "ssh:proxyjump", default, skip_serializing_if = "Vec::is_empty")]
462    pub ssh_proxy_jump: Vec<String>,
463
464    #[serde(rename = "ssh:userknownhostsfile", default, skip_serializing_if = "Vec::is_empty")]
465    pub ssh_user_known_hosts_file: Vec<String>,
466
467    #[serde(rename = "ssh:globalknownhostsfile", default, skip_serializing_if = "Vec::is_empty")]
468    pub ssh_global_known_hosts_file: Vec<String>,
469}
470
471/// Configuration error from parsing.
472#[derive(Debug, Clone, Default, Serialize, Deserialize)]
473pub struct ConfigError {
474    pub file: String,
475    pub err: String,
476}
477
478/// Webhook integration configuration.
479#[allow(dead_code)]
480#[derive(Debug, Clone, Default, Serialize, Deserialize)]
481pub struct WebhookConfigType {
482    #[serde(default)]
483    pub version: String,
484
485    #[serde(rename = "workspaceId", default)]
486    pub workspace_id: String,
487
488    #[serde(rename = "authToken", default)]
489    pub auth_token: String,
490
491    #[serde(rename = "cloudEndpoint", default)]
492    pub cloud_endpoint: String,
493
494    #[serde(default)]
495    pub enabled: bool,
496
497    #[serde(default)]
498    pub terminals: Vec<String>,
499}
500
501// ---- Full config container ----
502
503/// Complete application configuration.
504/// Matches Go's `wconfig.FullConfigType`.
505#[derive(Debug, Clone, Default, Serialize, Deserialize)]
506pub struct FullConfigType {
507    #[serde(default)]
508    pub settings: SettingsType,
509
510    #[serde(rename = "mimetypes", default)]
511    pub mime_types: HashMap<String, MimeTypeConfigType>,
512
513    #[serde(rename = "defaultwidgets", default)]
514    pub default_widgets: HashMap<String, WidgetConfigType>,
515
516    #[serde(default)]
517    pub widgets: HashMap<String, WidgetConfigType>,
518
519    #[serde(default)]
520    pub presets: HashMap<String, MetaMapType>,
521
522    #[serde(rename = "termthemes", default)]
523    pub term_themes: HashMap<String, TermThemeType>,
524
525    #[serde(default)]
526    pub connections: HashMap<String, ConnKeywords>,
527
528    #[serde(default)]
529    pub bookmarks: HashMap<String, WebBookmark>,
530
531    #[serde(rename = "configerrors", default, skip_serializing_if = "Vec::is_empty")]
532    pub config_errors: Vec<ConfigError>,
533}